﻿using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using Volo.Abp.DependencyInjection;

namespace Xms.Abp.Core;

/// <summary>
/// 0000-0000 0000 0000 0000 0000-0000 0000 0000-0000 0000-0000 0000 0000 0000 0000
/// 前4位全0
/// 20位为从2000年1月1日1时到当前的小时数，最大支持1048576小时即119年有余
/// 12位为当天的第几个秒
/// 8位为WorkerID，最多支持255个集群节点
/// 20位为秒内的序列号，每秒最多能产生1048576个
/// 生成的18位10进制值在0到999999999999999999之间
/// 单集群节点每秒最多能产生1048576个
/// </summary>
public class SnowflakeSequenceProvider : ISequenceProvider, ISingletonDependency
{
    protected const int HOURSTAMP_BITS = 20;
    protected const int SECONDSTAMP_BITS = 12;
    protected const int WORKER_ID_BITS = 8;
    protected const int SEQUENCE_BITS = 20;
    protected const int HOURSTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS + SECONDSTAMP_BITS;
    protected const int SECONDSTAMP_LEFT_SHIFT = SEQUENCE_BITS + WORKER_ID_BITS;
    protected const int WORKER_ID_LEFT_SHIFT = SEQUENCE_BITS;
    protected const int MAX_WORKER_ID = (1 << WORKER_ID_BITS) - 1;
    protected const int MAX_GENERATE_SEQUENCE = (1 << SEQUENCE_BITS) - 1;

    protected long WorkerId { get; }
    protected int Sequence = 0;
    private (long HourStamp, long SecondStamp) _lastTimestamp = (-1L, -1);

    public string Name => "Snowflake";

    public SnowflakeSequenceProvider(IOptions<SnowflakeSequenceOptions> options)
    {
        if (options.Value.WorkerId > MAX_WORKER_ID || options.Value.WorkerId < 0)
        {
            throw new Exception($"Worker Id can't be greater than {MAX_WORKER_ID} or less than 0.");
        }
        WorkerId = options.Value.WorkerId;
        _lastTimestamp = (GetTimestamp().HourStamp, -1L);
    }
    public long Create()
    {
        lock (this)
        {
            var stamp = GetTimestamp();
            if (stamp.HourStamp == _lastTimestamp.HourStamp && stamp.SecondStamp == _lastTimestamp.SecondStamp)
            {
                Sequence = (Sequence + 1) & MAX_GENERATE_SEQUENCE;
                if (Sequence == 0)
                {
                    stamp = TillNextSecond(_lastTimestamp);
                }
            }
            else
            {
                Sequence = 0;
            }
            if (stamp.HourStamp < _lastTimestamp.HourStamp)
            {
                throw new Exception(string.Format("Clock moved backwards. Refusing to generate id for {0} hours",
                    _lastTimestamp.HourStamp - stamp.HourStamp));
            }
            if (stamp.SecondStamp < _lastTimestamp.SecondStamp)
            {
                throw new Exception(string.Format("Clock moved backwards. Refusing to generate id for {0} seconds",
                    _lastTimestamp.SecondStamp - stamp.SecondStamp));
            }
            this._lastTimestamp = stamp;
            var nextId = Generate(stamp, Sequence);
            return nextId;
        }
    }

    public IEnumerable<long> Create(int quantity)
    {
        if (quantity <= 0 || quantity > MAX_GENERATE_SEQUENCE)
        {
            throw new Exception($"Parameter quantity can't be greater than {MAX_GENERATE_SEQUENCE} or less than 0 or equal to 0.");
        }
        lock (this)
        {
            var stamp = GetTimestamp();
            if (stamp.HourStamp == _lastTimestamp.HourStamp && stamp.SecondStamp == _lastTimestamp.SecondStamp)
            {
                Sequence = (Sequence + 1) & MAX_GENERATE_SEQUENCE;
                if (Sequence == 0)
                {
                    stamp = TillNextSecond(_lastTimestamp);
                }
            }
            else
            {
                Sequence = 0;
            }
            if (stamp.HourStamp < _lastTimestamp.HourStamp)
            {
                throw new Exception(string.Format("Clock moved backwards. Refusing to generate id for {0} hours",
                    _lastTimestamp.HourStamp - stamp.HourStamp));
            }
            if (stamp.SecondStamp < _lastTimestamp.SecondStamp)
            {
                throw new Exception(string.Format("Clock moved backwards. Refusing to generate id for {0} seconds",
                    _lastTimestamp.SecondStamp - stamp.SecondStamp));
            }
            this._lastTimestamp = stamp;
            var subplus = MAX_GENERATE_SEQUENCE - Sequence;
            var nextIds = new List<long>();
            void Generates((long HourStamp, long SecondStamp) stamp, int numbers)
            {
                for (int i = 0; i < numbers; i++)
                {
                    nextIds.Add(Generate(stamp, Sequence + i));
                }
            }
            if (quantity > subplus)
            {
                Generates(stamp, subplus);
                Sequence = 0;
                stamp = TillNextSecond(stamp);
                subplus = quantity - subplus;
                Generates(stamp, subplus);
                Sequence = subplus - 1;
            }
            else
            {
                Generates(stamp, quantity);
                Sequence = quantity - 1;
            }
            return nextIds;
        }
    }

    protected virtual long Generate((long HourStamp, long SecondStamp) stamp, long sequence)
    {
        return stamp.HourStamp << HOURSTAMP_LEFT_SHIFT | stamp.SecondStamp << SECONDSTAMP_LEFT_SHIFT | WorkerId << WORKER_ID_LEFT_SHIFT | sequence;
    }


    /// <summary>
    /// 获取下一秒时间戳
    /// </summary>
    /// <param name="lastTimestamp"></param>
    /// <returns></returns>
    protected static (long HourStamp, long SecondStamp) TillNextSecond((long HourStamp, long SecondStamp) lastTimestamp)
    {
        var timestamp = GetTimestamp();
        while (timestamp.SecondStamp <= lastTimestamp.SecondStamp)
        {
            timestamp = GetTimestamp();
        }
        return timestamp;
    }

    /// <summary>
    /// 生成时间戳
    /// </summary>
    /// <returns></returns>
    protected static (long HourStamp, long SecondStamp) GetTimestamp()
    {
        var now = DateTime.UtcNow;
        var hourStamp = (long)(now - new DateTime(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc)).TotalHours;
        var secondStamp = (long)(DateTime.UtcNow - new DateTime(now.Year, now.Month, now.Day, now.Hour, 0, 0, DateTimeKind.Utc)).TotalSeconds;

        return (hourStamp, secondStamp);
    }
}
